package com.tianji.aigc.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.tianji.aigc.config.ai.SystemPromptConfig;
import com.tianji.aigc.config.ai.ToolResultHolder;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.service.ChatSessionService;
import com.tianji.aigc.entity.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@RequiredArgsConstructor
@Service
public class ChatServiceImpl implements ChatService {

    private final ChatClient dashScopeChatClient;
    private final ChatClient openAiChatClient;
    private final SystemPromptConfig systemPromptConfig;
    // 存储大模型的生成状态，这里采用ConcurrentHashMap是确保线程安全,用来标记某个会话是否正在生成内容
    private static final Map<String, Boolean> GENERATE_STATUS = new ConcurrentHashMap<>();
    private final ChatMemory chatMemory;
    private final VectorStore vectorStore;
    private final ChatSessionService chatSessionService;
    // 输出结束的标记
    private static final ChatEventVO STOP_EVENT = ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build();

    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        //获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器，用于在输出中断后的数据存储
        StringBuilder outputBuilder = new StringBuilder();
        //生成请求id
        String requestId = IdUtil.fastSimpleUUID();
        // 获取用户id
        var userId = UserContext.getUser();
        //补充一次会话的标题并更新时间
        chatSessionService.update(sessionId, question, userId);
        return dashScopeChatClient.prompt()//创建一个聊天提示
                .system(promptSystem -> promptSystem
                        .text(systemPromptConfig.getChatSystemMessage().get())// 设置系统提示语
                        .param("now", DateUtil.now()))// 设置当前时间的参数

                .advisors(advisor-> advisor
                        //添加一个工具，用于从向量库ES中搜索匹配的内容
                        .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder().query("").topK(999).build()))
                        //通过 param(...) 将当前会话ID绑定到对话上下文中，用于记忆管理。拼接上下文
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))

                //设置工具上下文作用是在给工具方法提供需要的参数比如当前用户 ID，请求 ID
                .toolContext(MapUtil.<String, Object>builder()
                        .put(Constant.REQUEST_ID, requestId)
                        .put(Constant.USER_ID, userId) // 设置用户id参数
                        .build())
                .user(question)//设置用户输入的问题
                .stream()//开启流式响应
                .chatResponse()//获取聊天响应
                .doFirst(() -> {
                    // 在开始生成之前，先将当前会话的生成状态设置为 true，表示正在生成中
                    GENERATE_STATUS.put(sessionId, true);
                })
                .doOnComplete(() ->{
                    GENERATE_STATUS.remove(sessionId);// 在生成完成后移除生成状态标记
                })
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId))// 在发生错误时移除生成状态标记
                .doOnCancel(() -> {
                    // 当输出被取消时，保存输出的内容到redis的历史记录中
                    saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                // 输出过程中，判断是否正在输出，如果正在输出，则继续输出，否则结束输出
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {// 将响应转换为 ChatEventVO
                    // 对于响应结果进行处理，如果是最后一条数据，就把此次消息id放到内存中
                    // 当 AI 模型生成一条完整的消息（响应结束）时，
                    // 把这条消息的 messageId 和当前请求的 requestId 建立映射关系，保存到 ToolResultHolder 中。
                    // 从而解决，在历史聊天记录中没有外参数
                    // params（就是课程下单的信息）的问题注意这里前端有返回数据redis没有保存
                    var finishReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if(StrUtil.equals(Constant.STOP, finishReason)){
                        var messageId = chatResponse.getMetadata().getId();
                        ToolResultHolder.put(messageId, Constant.REQUEST_ID, requestId);
                    }
                    //获取大模型的输出内容
                    String text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    //封装成我们返回信息的形式
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                }).concatWith(
                        Flux.defer(() ->{
                            //通过请求参数id获取参数列表，如果不为空就把其追加到返回结果中
                            var map = ToolResultHolder.get(requestId);
                            if(CollUtil.isNotEmpty((Map<?, ?>) map)){
                                ToolResultHolder.remove(requestId);
                                //响应给前端课程介绍小卡片的特定格式JSON在封装以后再追加的
                                ChatEventVO chatEventVO = ChatEventVO.builder()
                                        .eventData(map)
                                        .eventType(ChatEventTypeEnum.PARAM.getValue())
                                        .build();
                                return Flux.just(chatEventVO, STOP_EVENT);
                            }
                            return Flux.just(STOP_EVENT);
                        }));
        //在流式接口中，客户端通常无法知道流什么时候结束，除非服务端主动发送一个结束标记。客户端监听到这个 STOP 事件后，
        // 就知道“这次聊天响应已经结束了”，可以做一些 UI 上的收尾处理，比如隐藏加载动画、允许再次提问等。
    }
    @Override
    public String chatText(String question) {
        return this.openAiChatClient.prompt()
                .system(promptSystem -> promptSystem.text(this.systemPromptConfig.getTextSystemMessage().get()))
                .user(question)
                .call()
                .content();
    }
    @Override
    public void stop(String sessionId) {
        // 移除标记
        GENERATE_STATUS.remove(sessionId);
    }

    /**
     * 保存停止输出的记录
     *
     * @param conversationId 会话id
     * @param content        大模型输出的内容
     */
    private void saveStopHistoryRecord(String conversationId, String content) {
        chatMemory.add(conversationId, new AssistantMessage(content));
    }


}
